unit FindWriteln;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils;

type
  TFindWritelnLog = procedure(EventType : TEventType; const Msg: string) of object;

function FindWritelnInDirectory(Dir: string; Recurse: boolean; const Log: TFindWritelnLog): integer;

implementation

function ReadNextToken(const Src: string; var SrcP: PChar; var Line: integer): string;
var
  p, TokenStart: PChar;
begin
  p:=SrcP;
  while p^ in [' ',#9] do inc(p);
  repeat
    case p^ of
    #0:
      if p-PChar(Src)=length(Src) then begin
        SrcP:=p;
        exit('');
      end
      else
        inc(p);
    #10,#13:
      begin
      inc(Line);
      if (p[1] in [#10,#13]) and (p^<>p[1]) then
        inc(p,2)
      else
        inc(p);
      end;
    ' ',#9:
      inc(p);
    else
      break;
    end;
  until false;
  TokenStart:=p;
  case p^ of
  'a'..'z','A'..'Z','_':
    while p^ in ['a'..'z','A'..'Z','_','0'..'9'] do inc(p);
  '0'..'9':
    while p^ in ['0'..'9'] do inc(p);
  '''':
    begin
    inc(p);
    repeat
      case p^ of
      #0,#10,#13: break;
      '''':
        begin
        inc(p);
        break;
        end;
      end;
      inc(p);
    until false;
    end;
  '/':
    if p[1]='/' then begin
      inc(p,2);
      while not (p^ in [#0,#10,#13]) do inc(p);
    end else
      inc(p);
  '{':
    begin
      inc(p);
      repeat
        case p^ of
        #0:
          if p-PChar(Src)=length(Src) then begin
            SrcP:=p;
            exit('');
          end;
        #10,#13:
          begin
          inc(Line);
          if (p[1] in [#10,#13]) and (p^<>p[1]) then
            inc(p);
          end;
        '}': break;
        end;
        inc(p);
      until false;
      inc(p);
    end;
  '(':
    if p[1]='*' then begin
      inc(p,2);
      repeat
        case p^ of
        #0:
          if p-PChar(Src)=length(Src) then begin
            SrcP:=p;
            exit('');
          end;
        #10,#13:
          begin
          inc(Line);
          if (p[1] in [#10,#13]) and (p^<>p[1]) then
            inc(p);
          end;
        '*':
          if p[1]=')' then break;
        end;
        inc(p);
      until false;
      inc(p,2);
    end else
      inc(p);
  else
    inc(p);
  end;
  SetLength(Result,p-TokenStart);
  Move(TokenStart^,Result[1],length(Result));
  SrcP:=P;
end;

procedure GetLineStartEndAtPosition(const Source:string; Position:integer;
   out LineStart,LineEnd:integer);
begin
  if Position<1 then begin
    LineStart:=0;
    LineEnd:=0;
    exit;
  end;
  if Position>length(Source)+1 then begin
    LineStart:=length(Source)+1;
    LineEnd:=LineStart;
    exit;
  end;
  LineStart:=Position;
  while (LineStart>1) and (not (Source[LineStart-1] in [#10,#13])) do
    dec(LineStart);
  LineEnd:=Position;
  while (LineEnd<=length(Source)) and (not (Source[LineEnd] in [#10,#13])) do
    inc(LineEnd);
end;

function GetLineInSrc(const Source: string; Position: integer): string;
var
  LineStart, LineEnd: integer;
begin
  GetLineStartEndAtPosition(Source,Position,LineStart,LineEnd);
  Result:=copy(Source,LineStart,LineEnd-LineStart);
end;

function CheckFile(Filename: string; const Log: TFindWritelnLog): integer;
var
  Token, LastToken, Src: String;
  ms: TMemoryStream;
  p: PChar;
  Line, LastIFDEF, AllowWriteln: Integer;
  Lvl, VerboseLvl: integer;
begin
  Result:=0;
  ms:=TMemoryStream.Create;
  try
    ms.LoadFromFile(Filename);
    if ms.Size=0 then exit;
    Src:='';
    SetLength(Src,ms.Size);
    Move(ms.Memory^,Src[1],length(Src));
    p:=PChar(Src);
    AllowWriteln:=0;
    Line:=1;
    LastIFDEF:=-1;
    Token:='';
    Lvl:=0;
    VerboseLvl:=-1;
    repeat
      LastToken:=Token;
      Token:=ReadNextToken(Src,p,Line);
      if Token='' then break;
      if Token[1]='{' then begin
        Token:=lowercase(Token);
        if Token='{allowwriteln}' then begin
          if AllowWriteln>0 then begin
            inc(Result);
            Log(etError,Filename+'('+IntToStr(Line)+'): writeln already allowed at '+IntToStr(AllowWriteln)+': '+GetLineInSrc(Src,p-PChar(Src)+1));
          end;
          AllowWriteln:=Line;
        end
        else if Token='{allowwriteln-}' then begin
          if AllowWriteln<1 then begin
            inc(Result);
            Log(etError,Filename+'('+IntToStr(Line)+'): writeln was not allowed: '+GetLineInSrc(Src,p-PChar(Src)+1));
          end;
          AllowWriteln:=0;
        end
        else if SameText(LeftStr(Token,4),'{$if') then begin
          inc(Lvl);
          LastIFDEF:=Line;
          if SameText(LeftStr(Token,15),'{$ifdef Verbose') then begin
            if VerboseLvl<0 then VerboseLvl:=Lvl;
          end;
        end else if SameText(LeftStr(Token,6),'{$else') then begin
          if Lvl=VerboseLvl then
            VerboseLvl:=-1;
          LastIFDEF:=Line;
        end else if SameText(LeftStr(Token,7),'{$endif') then begin
          if Lvl=VerboseLvl then begin
            VerboseLvl:=-1;
          end;
          dec(Lvl);
        end;
      end
      else begin
        if (CompareText(Token,'str')=0) and (LastToken<>'.') then begin
          if byte(Line-LastIFDEF) in [0,1] then begin
            // ignore writeln just behind IFDEF
            LastIFDEF:=Line;
          end;
        end;
        if (CompareText(Token,'writeln')=0)
            and (LastToken<>'.')
            and (LastToken<>':=')
            and (LastToken<>'=')
            and (LastToken<>'+')
            and not SameText(LastToken,'function')
            and not SameText(LastToken,'procedure') then begin
          if Lvl=VerboseLvl then begin
            // ignore writeln inside $IFDEF VerboseX
          end else if byte(Line-LastIFDEF) in [0,1] then begin
            // ignore writeln just behind IFDEF
            LastIFDEF:=Line;
          end else if AllowWriteln<1 then begin
            inc(Result);
            Log(etError,Filename+'('+IntToStr(Line)+'): '+GetLineInSrc(Src,p-PChar(Src)+1));
          end;
        end;
      end;
    until false;
  finally
    ms.Free;
  end;
end;

function FindWritelnInDirectory(Dir: string; Recurse: boolean; const Log: TFindWritelnLog): integer;
var
  Info: TRawByteSearchRec;
  Ext: String;
begin
  Result:=0;
  Dir:=IncludeTrailingPathDelimiter(Dir);
  if FindFirst(Dir+AllFilesMask,faAnyFile,Info)=0 then begin
    repeat
      if (Info.Name='') or (Info.Name='.') or (Info.Name='..') then continue;
      if (Info.Attr and faDirectory)>0 then begin
        if Recurse then
          Result+=FindWritelnInDirectory(Dir+Info.Name,true,Log);
      end
      else begin
        Ext:=lowercase(ExtractFileExt(Info.Name));
        case Ext of
        '.p','.pp','.pas','.inc': Result+=CheckFile(Dir+Info.Name,Log);
        end;
      end;
    until FindNext(Info)<>0;
    FindClose(Info);
  end;
end;

end.


